[포스코x코딩온] 웹 풀스택 과정 7기 6주차 화요일 회고
목차
TypeScript
과정에서 배우지는 않았지만 내용을 따라하면서 TypeScript를 사용해보았다.
그런데 무작정 사용하려고 하니 오류가 무더기로 발생했다.
이를 해결하기 위한 고군분투의 과정을 기록해둔다.
*.ts 파일 실행 오류
당연히 node가 *.ts
파일을 실행해 줄거라고 생각했는데 전혀 그렇지 않았다.
*.ts
파일을 실행하려면 몇 가지 과정이 필요했다.
먼저 typescript
와 ts-node
를 설치했다.
그리고 프로젝트 루트 폴더에서 tsc --init
명령어로 tsconfig.json
파일을 생성했다.
이후 ts-node
로 *.ts
파일을 실행시킬 수 있었다.
npm i typescript ts-node
npx tsc --init
npx ts-node src/index.ts
절대경로
매번 상대경로를 쓰다보니 import 경로가 너무 길어졌다.
그런데 React를 공부하다보니 절대경로를 사용하는 방법이 있어서 찾아보았다.
tsconfig.json
파일에 다음과 같은 설정을 추가하면 된다.
{
"compilerOptions" : {
"baseUrl" : "./" ,
"paths" : {
"@/*" : [ "*" ]
}
}
}
이 설정을 추가하면 @/
로 시작하는 경로를 절대경로로 사용할 수 있다.
다만 Node는 절대경로를 지원하지 않기 때문에 tsconfig-paths
라는 라이브러리를 사용해야 한다.
나는 다음 명령어를 index.sh
파일에 추가해두고 사용했다.
ts-node -r tsconfig-paths/register --files index.ts
Sequelize
Sequelize는 Node.js에서 SQL을 사용하기 위한 ORM(Object-Relational Mapping) 라이브러리이다.
npx sequelize init
명령어를 통해 Sequelize를 초기화할 수 있다.
해당 명령어를 실행하면 여러가지 파일들이 생성된다.
이후 models
폴더 안에 사용하고 싶은 테이블에 맞춰 다음과 같은 파일을 생성한다.
import { DataTypes, Sequelize } from "sequelize" ;
export default function TableName (sequelize : Sequelize, dataTypes : typeof DataTypes ) {
return sequelize. define ( "TableName" , {
/*
열이름: {
속성: 속성값
}
*/
id: {
type: dataTypes. INTEGER , // 타입
autoIncrement: true , // 자동 증가
primaryKey: true , // 기본키
allowNull: false // null 허용 여부
},
name: {
type: dataTypes. STRING ( 20 ),
}
// ...
}, {
tableName: "TableName" , // 테이블 이름
freezeTableName: true , // 테이블 이름 고정 (false 일 경우 테이블 이름을 복수로 만들어버림)
timestamps: false , // createdAt, updatedAt 컬럼 생성 여부
});
}
이후 다음과 같이 사용할 수 있다.
import db from "@/models" ;
async function get ( req : Request , res : Response ) {
const row = await db.TableName. findByPk ( /* pk */ ); // 기본키 값으로 조회
const rows = await db.TableName. findAll (); // 전체 조회
const rows = await db.TableName. findAll ({where: { /* 조건 */ },}); // 조건에 맞는 행 조회
const row = await db.TableName. create ({ /* 데이터 */ }); // 데이터 생성
await db.TableName. update ({ /* 수정할 데이터 */ }, {where: { /* 조건 */ },}); // 조건에 맞는 데이터 수정
await db.TableName. destroy ({where: { /* 조건 */ },}); // 조건에 맞는 데이터 삭제
}
models/index.js
뜯어보기
Sequelize 초기화 시 생성되는 파일 중 models/index.js
라는 파일이 있다.
해당 파일을 import 하면 DB 객체를 얻을 수 있다.
그러나 내 프로젝트에서는 해당 파일에서 오류가 나왔다.
오류를 고치는 김에 TS로 작성하기 위해 직접 뜯어 봤다.
'use strict' ;
const fs = require ( 'fs' );
const path = require ( 'path' );
const Sequelize = require ( 'sequelize' );
const process = require ( 'process' );
const basename = path. basename (__filename);
const env = process.env. NODE_ENV || 'development' ;
const config = require (__dirname + '/../config/config.json' )[env];
// config.json 파일을 불러와서 현재 환경에 맞는 설정을 가져옴
const db = {};
let sequelize; // config 파일에 맞춰 sequelize 객체 생성
if (config.use_env_variable) {
sequelize = new Sequelize (process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize (config.database, config.username, config.password, config);
}
fs
. readdirSync (__dirname)
. filter ( file => {
return (
// models 폴더 안에 있는 파일들 중 index.js, *.test.js 파일은 제외한 모든 .js 파일을 불러옴
file. indexOf ( '.' ) !== 0 &&
file !== basename &&
file. slice ( - 3 ) === '.js' &&
file. indexOf ( '.test.js' ) === - 1
);
})
. forEach ( file => {
// 각 파일들을 불러와서 해당 파일의 default 함수에 sequelize, DataTypes 객체를 넘겨줌
const model = require (path. join (__dirname, file))(sequelize, Sequelize.DataTypes);
// 그 값을 db 객체에 넣어줌
db[model.name] = model;
});
Object. keys (db). forEach ( modelName => {
// db 객체에 있는 모델들을 순회하면서 associate 메소드가 있는 경우 이를 실행
if (db[modelName].associate) {
db[modelName]. associate (db);
}
});
db.sequelize = sequelize; // db 객체에 sequelize 객체를 넣어줌
db.Sequelize = Sequelize;
module . exports = db; // db 객체를 export
먼저 나는 "type": "module"를 지정했기 때문에 require
를 import
로 바꿔야 했다.
그렇게 되면 중간의 forEach 문에서 폴더 내 파일들을 불러올 수 없었다.
그래서 그냥 수동으로 불러왔다.
// models/index.ts
import { Sequelize, DataTypes } from "sequelize" ;
import process from "process" ;
import Visitor from "./visitor" ;
...
const visitor = Visitor (sequelize, DataTypes);
...
config.json
파일도 config/index.ts
파일로 바꿔서 불러왔다.
// config/index.ts
import { Dialect } from "sequelize" ;
interface Config {
username : string ;
password : string ;
database : string ;
host : string ;
dialect : Dialect ;
}
interface Configs {
[ key : string ] : Config ;
}
const configs : Configs = {
development: {
username: "u1" ,
password: "1" ,
database: "exerdb" ,
host: "localhost" ,
dialect: "mysql" ,
},
// test: {},
// production: {},
}
export default configs;
// models/index.ts
...
import configs from "@/config" ;
...
이를 수정했더니 fs
, path
모듈은 필요 없어져서 제거했다.
또 TS를 적용하니 sequelize
객체를 생성할 때 오류가 발생했다.
따라서 envVariable
변수를 따로 빼줬다.
...
const { database , username , password , use_env_variable , ... config } = configs[env];
const envVariable = use_env_variable && process.env[use_env_variable]
const sequelize = envVariable
? new Sequelize (envVariable, config)
: new Sequelize (database, username, password, config);
...
마지막으로 DB의 타입을 정의하고 export 해줬다.
...
interface DB {
sequelize : Sequelize ;
Sequelize : typeof Sequelize;
visitor : typeof visitor;
}
export default <DB>{
sequelize ,
Sequelize ,
visitor ,
} ;
최종 코드는 다음과 같다.
import { Sequelize, DataTypes } from "sequelize" ;
import process from "process" ;
import configs from "@/config" ;
import Visitor from "./visitor" ;
const env = process.env. NODE_ENV || "development" ;
const { database , username , password , use_env_variable , ... config } = configs[env];
const envVariable = use_env_variable && process.env[use_env_variable]
const sequelize = envVariable
? new Sequelize (envVariable, config)
: new Sequelize (database, username, password, config);
const visitor = Visitor (sequelize, DataTypes);
interface DB {
sequelize : Sequelize ;
Sequelize : typeof Sequelize;
visitor : typeof visitor;
}
export default <DB>{
sequelize ,
Sequelize ,
visitor ,
} ;
암호화
보안 문제는 어디서나 일어날 수 있는데 이는 서버 상에서도 마찬가지이다.
따라서 서버에서도 보안을 위해 암호화를 해야 한다.
Node 에서는 이를 위해 crypto
모듈을 제공한다.
crypto
import crypto from "crypto" ;
function createSaltHash ( pw : string ) {
const salt = crypto. randomBytes ( 8 ). toString ( "hex" );
const hash = crypto. pbkdf2Sync (pw, salt, 100000 , 64 , "sha512" ). toString ( "hex" );
return { salt, hash };
}
function comparePassword ( pw : string , salt : string , hash : string ) {
const hashedPW = crypto. pbkdf2Sync (pw, salt, 100000 , 64 , "sha512" ). toString ( "hex" );
return hashedPW === hash;
}
createSaltHash
함수를 통해 salt 값과 pw의 해시값을 생성한 뒤 유저의 정보로 저장한다.
이후 비밀번호를 확인할 때 comparePassword
함수를 통해 비밀번호가 일치하는지 확인한다.
bcrypt
bcrypt
는 외부 암호화 라이브러리이다.
crypto
보다 쉽게 사용할 수 있다는 장점이 있다.
import bcrypt from "bcrypt" ;
async function createSaltHash ( pw : string ) {
const salt = await bcrypt. genSalt ( 8 );
const hash = await bcrypt. hash (pw, salt);
return { salt, hash };
}
async function comparePassword ( pw : string , salt : string , hash : string ) {
const hashedPW = await bcrypt. hash (pw, salt);
return hashedPW === hash;
}
메소드 이름 뒤에 Sync
를 붙이면 동기함수로 사용할 수 있다.
하지만 공식문서 는 비동기 함수를 권장한다.
이벤트 루프를 막아 앱의 성능을 저하시킬 수 있기 때문이다.